ルールズ・オブ・プログラミング 読書
1章:コードは単純であるべきだが、単純化はするな
「単純である」とは
1. 誰が読んでも簡単にスラスラ理解できる
2. バグがない状態に至るまでの時間が早い
3. コード量も少ない
4. なおかつ、問題を解決できる状態(非機能面まで含めて)
解法を単純にする前に問題を単純にできるか考えろ
全てのパターンに対して簡単な解法を作ろうとしてないか?そのパターンへの解放は本当に必要なのか?
引数aの範囲0~10までに対する解法があればいいのに、100とか10000とかにも対応できるようにして無いか?
「単純化するな」とは
上記の「単純である」を損なうような、見せかけの単純化をやめろってことやと思うonigiri.w2.icon
例えば以下2つ。
何でもかんでも関数化してしまい、コードを読む人がスラスラ理解しにくい状態を作る
めっちゃ簡単なコードだが、引数が増えるごとに処理時間が増えて、肝心の問題を解決できなくなる
見せかけの単純化をやめろって話。
todo.icon アルゴリズムとデータ構造に関する知識、それ以外の「解法」に関する知識をもっと蓄えておく必要がある。
より多くの「解法」「知識」が無いと、より単純なコードを書ける場面で書けないことになる。
leetCodeを毎日練習しなくては
todo.icon 再帰処理に対する経験を増やし、「解法」としてすぐ頭に思い浮かべるレベルにしておく
再帰はプロなら自由に扱えるべき
todo.icon 読みやすい名前をつけるためには、英語力がないとダメ。もしくは別の何か、OSSよんで命名パターンの経験値を高めるとか。
英語力無いと単語とか思い浮かばないし、文法もぐちゃぐちゃになる。
2章:バグは感染する
1つのバグの存在が、ジワジワと関連するコードに波及していくことを「感染する」という。
1つのバグが、他箇所の新しいバグに繋がっていく。
バグありコードを使う側は、そのバグに依存したコードを書くようになる。そもそも正常じゃない状態で新しいコードが生まれる。これは臭い。新しいバグに繋がりそうだよな。
どう繋がるのか?
例えば、元凶のバグを見つけて治したとする。
でも、元々バグありコードを使ってたやつらはどうなる?バグあり前提でコードが書かれてるため、ここで不破が起こる。
バグがなくなった途端、想定外の動きになってしまい、ここで新しいバグが生まれる。
元凶バグが早期に見つからず放置される時間が長いほど、このバグの感染拡大は大きく手がつけられなくなる。
だからこそ、バグを早期発見できるように努めることはとても大事である。
バグを早期発見する、そして生み出しにくくするために。
1. ステートレスなコードを心がけ、テストを書きやすくしておく
「状態」は諸悪の根源だと思う。状態が存在するだけで、不整合が起きやすくなる。
テストもしにくいし、良いことがない。
保守性の高いコードの基本は、「純粋な関数」にあり。副作用のない純粋なコードこそ、保守性を高める。
2. どうしても状態を持つ必要があり、テストコードを書くのがしんどいなら、「assertion(内部テスト)」を利用する
クラスに状態を持たせてしまうと、その時点で瓦解が始まる。
例えば、メソッドを実行して状態を更新するとかなった時、メソッドにミスがあって状態が不整合な状態になったとしよう。この時点でシステムが腐る。
不変条件、事後条件をメソッドに守らせるという形にしたら良いのではないか。そこでassertionの出番。
assertionを使って、メソッド実行最後に状態チェックを行い、バグを孕んでないかチェックすることができる。
また、assertion文は、開発時のみ有効にして本番では無効にするってやり方も取れるので良い。
ただし、本番中もバグの早期発見のために有効にしておいた方がいい説もある。
ここは、性能との噛み合い次第。
3. 呼び出し側を信用せず、どんな使い方されても頑強なものにしとく
全て、そのクラス、関数で完結する状態にしておけ。
クラスのメソッドの呼び出し順によって、得られる結果が変わるみたいな、呼び出し側に成功の可否を委ねることは絶対にするな。
4. 未定義を返してしまうようなら下手なインターフェースと言わざる負えない
できる限り未定義という値を返さないようにしろって話。
これあんまデメリットを言語化できてない。
todo_done.icon nullの扱いに関してのプラクティスを知って使いこなせる状態に
todo_done.icon typescriptをもっと保守的にするためには、ts-effectを学ぶ必要があるのかもしれない 例外を明示的に外部に示せないが故に、実行時エラーまでエラーを発見できない可能性あり。
これは必要ない。ts-effect学ぶくらいなら、関数型プログラミング言語を先に学べonigiri.w2.icon
todo.icon テストする際はやはり、入力用データセットをあらかじめ設計してた方がやりやすいのかな
本番環境時には無効化する機構まで含めて導入しておけよ。
todo.icon コミット時にバグチェックとかできる状態にしてると最高。
バグが紛れ込んだ状態でコミットするの、後々面倒くさいことになるから必要かも。
少なくとも静的チェックくらいは。
todo.icon 「例外処理」について今、もっと深く調べるべき。特に「Go」や関数型プログラミングの考え方は参考になるかも。
3. 優れた名前こそ最高のドキュメントである
「名付けは大事」って皆なんとなくわかってると思うが、その認識は100倍甘い。
完全に最高な名前がつけられてないものが存在するだけで、コード読解の負荷が上がりまくる。
読解負荷が上がると、生産性が落ちる、追加/改修の際にバグが入りやすくなる。よりどりみどりである。
多分、みんな、ここまで言っても理解しきれてない。重要性の理解が足りてない。
名付けこそが一番重要と言っても過言ではない。ここを疎かにして時点でシステムの腐敗が始まる。
ファイル名、変数名、クラス名、ありとあらゆる箇所の名前を適切につけること。
命名規則を舐めちゃいけない。もっと機械的に一貫的に名付けできるようルールを決める。
todo.icon 命名規則に関するあれこれを調べて自分の方針を決めようか。
4. 一般化するなら、少なくとも3つのケースが必要
一般化ってのは
今必要十分な関数を作るのではなく、「後にこういうケースもあるだろうから」で他ケースにも対応できる関数を作ること
YAGNIとも言われてるように、その一般化は絶対に必要ない 必要になった時に一般化を考えるべき
更に、必要なケースのみを使って一般化を考えるべきで、3つのケースが出た場合でも、存在しないケースを考慮するのはNG
「早すぎる一般化」はなぜダメなのか
1. 後に変更しにくくするリスクがある
依存が増えると、それだけ変更による影響範囲が広がるという前提を覚えておくこと。
一般化された関数を使う輩が多くなってると、その関数の変更時に痛い目見ることになる。
存在しないケースを妄想しながら作った一般化とか、全く持って意味のないものになりやすい。
2. それを作るのに費やした労力が無駄
結局使われない機能になることが多い。そんなものに時間をかけたのか?まじか?
生産性のダウンですな。
3. コード自体が単純ではなくなるし、抽象化しすぎて名付けしにくい
2次元程度の一般化ならまだマシかもしれんが、これが増えていくと読みにくくなることは必至。
さらに、この関数名をどうやってつけるのか。難しくなってくる。
抽象的すぎる関数名になった結果、結局関数の中身を読む必要が出てきたりする。
コードリーディングに弊害出る。
4. 要するに、一般化することによって、その関数がみるみるうちに太っていくことになるんだよ
一般化は、関連するユースケースが3つ以上出てきた際に初めて考えると良い。
「3つ」という数字は、まあ最低3つって感じ。別に3つでも一般化しなくてもいいかも。
実際にあるユースケースを使って一般化する方が確実に良いものができる。
常に必要十分を意識しろ。
さもないと、想定外の使われ方される関数とか出来上がったりするよ。気をつけて
5章:最適化はするな
最適化するなとは言ってないことに注意。最適化は必要な場面はいくらでもある。
いくらでもあるが、その最適化をするのはプロジェクトの最後にとっておけって話。
ただし、大まかの設計やアーキテクチャなどは、最初から性能を意識しておくことも大事。
そのアーキテクチャを採用した時点で、求められる性能要件を満たせないということもあり得るから。
性能気にせずに採用して突き進み、最後の最後に大きな手戻りすることになる悲劇に遭わないために。
6章の話に繋がる
それでも、最適化よりも単純なコードを意識して書くことを最優先にすべき。
単純で明快、理解しやすいコードを保っていれば、最適化が必要な段階で簡単に改善できる。
アーキ再考みたいな、どんでん返しでない限り、この意識で作っていけ。
最適化の手順
1. 性能を計測し、どこが性能低下の多くを占めているのか探す
2. バグがないことを確認する
バグによって大量に実行されてたりしない?みたいなのをちゃんと先に確認しとけ
3. 対象を深く理解する
最適化しようとしてる対象の動作やデータ構造などを深く理解しておくこと
対象を理解せずして、どうやって性能低下の原因や対策を見つけるというのか
4. 対象の最適化成功によって、どれくらいの性能向上が見込めるのか確認する
最適化を実施した結果、本当に求めてる性能向上につながるのかをテストしておく
対象コードの重い原因となってる処理を省いて簡単な結果を返すように変更してテストするなどしてみて。
その結果、性能向上につながることがわかったら、最適化実施サイクルにGO!!
5. 最適化 -> 計測 -> 結果比較 -> 最適化 -> ...のサイクルを繰り返す
いきなり大きくコードを変えるのはナンセンス。
怪しい箇所から小さく変更、計測、性能向上の確認、という1サイクルを繰り返す。
そして、求める性能向上に到達するまで繰り返すこと。
6. 求める性能結果になる最適化ができたら、完了!それ以上のサイクルには進まない
例え、別のさまざまな改善点を思いついたとしても、無視してサイクルストップすること。
そんなことしてたら、キリないからな。
まあ、性能について心配しすぎるな。なんとかなる。
と言ってお以下のようなケースもあるわけだが、大抵は単純さを優先した方がうまくいくと思われる。
6章:コードレビュー(対面)をどんどんやれい
コードレビューのメリット2つ
1. バグが見つかりやすくなる
2. チーム内で知識共有が活発になる
3. レビューのために読みやすい/恥ずかしくないコードを書くようになる
「2.」が最も重要
シニアの知識をジュニアに共有し、ジュニアの成長を促す。
シニア同士でも知識共有ができたりする。
ただし、ジュニア同士でのレビューはダメ。間違った知識共有とかされる可能性あるし、終わり。
7章:失敗が起こる場合をなくす
「使われる側」は使う側がアホで間違った使い方をするという前提で、より失敗回避を強制する設計をしろ。
失敗回避 = 例外を投げないってことじゃないからな。
事前条件をミスる阿呆がいるなら、すぐに例外を投げて開発中に簡単にミスを気づけるようにするのも大事。
使われる側の自由度が上がるほど、使う側が思い通りの使い方せずにバグを混在させる要因になる。
失敗例はいくらでもある
1. 文字列の長さを強制しない。
2. メソッドに暗黙的な実行順序がある(メソッドa -> メソッドbと言う順番じゃないと機能しない)
などなど
部品(関数、クラス、その他)を作ってる際は、使う側に意図通りの使い方を強制させれないか常に考えるべき。
もし、その設計で強制できないなら、もっと良いやり方があるはず。
コメントで注意喚起なんていう馬鹿なことしちゃいけないよ。それを読まれるはずがない。
8章:実行されてないコードは動作しない
誰からも参照されてないコードを放置せずに、きっちり削除しろ
システム改修が繰り返される中で、誰からも使われなくなるコードってのが出てくることが多くある。
こういうコードは絶対に削除しろ、削除しないと後々のバグに繋がる。
これらのコードは動作環境で全く動作に影響してない、影響してないから健在的なバグにならず放置され続ける。
でも、いつかのタイミングで「あ、なんかメソッドある使おう」となって使われると、途端にバグが浮かび上がったりする。
なぜこの問題が起きるのか?
それは、使う側は、すでに存在する関数に対して「正常だからまだ存在するんだな」と思うのが必然だから。
何ヶ月も生き残ってきたこのコードは、正常に動いてるという意識で使うもんだ。
でも実際には、誰からも使われてなかったからバグがたまたま顕在化してなかっただけってこともある。
必要なくなったコードは、そのタイミングですぐに削除されるべき。
残ってたら上記のような問題につながる。
「でも、テストしてたらバグ発生防げたんじゃ?」ってか。
いやいや、なんで使わないコードのためにテストを維持しないとあかんのだ。時間の無駄すぎる。
速攻捨てろ。捨てて然るべき。
「後で使うかもしれない?」。YAGNIを思い出せ。 todo.icon 誰からも参照されなくなったコードを発見できる開発ツールを探して適用しよう。eslintでありそう。
9章:集約可能なコードを書け
認知不可が少なく、短期記憶に優しい、読みやすいコードを書けって話。
コードのあっちこっちを行ったり来たり、短期記憶を入れ替え続けてやっと理解できるコードにはしない。読むのしんどいし、理解力不足でバグに繋がる。
何でもかんでも処理を抽象化して関数に分けていい訳じゃない
抽象化することで、結局何をやってるのか確実に判明せず、その関数の中身を読みに行かざるおえないことが多々ある。
そうなると短期記憶が爆発するだろう。
なので、処理を抽象化して名付けする前に、その抽象化によって今のコードの認知負荷が下がるのか自問しよう。
自問した結果下がると判断したなら、抽象化しましょう。
名付けとその一貫性、共通認識ってのも大事な要素。
sortと見れば並び替えの処理だとすぐにわかるよな。
1つのシステムの中で、そういうこの名前はこういう機能みたいな一貫性が作れてるほど、認知不可の少ないコードになる
ただし、「この名前ならこういう機能」という認識をぶっ壊すことはしない。
sortと書いてるのに、全く別の処理してたとかいうことになると間違った使われ方に繋がる。
10章:複雑性を局所化しろ
「少なくとも複雑性を完全に排除することなんてできない」ってことは頭に入れておく
現実の事象を表現しようとする際、いくら頑張っても排除できないことはある。
それを無理に単純化しようとすると、余計にわかりにくくなるので注意するように。
かといって、複雑性をいろんなところに点在させるのも良くない。ので...
複雑性は局所化して押さえ込めるよう努力しろ。
ある一部の複雑なロジックなどがあった際、それらが色んなところに点在して、共同で動いてるとかだるすぎる。
そういうのは、まとめて1つの機能にして局所化しろ。
その複雑化が、他に波及しないよう、いい感じに抑え込むんや。
複雑性を抑え込み、それを簡単に使えるインターフェースを公開する形でやりすごそう。
11章:2倍良くなるか?
当時採用したアーキテクチャや細かい方法が、現実の変化によってうまく噛み合わないようになる時はくる。
これは自然なものであり、何も当時の人間がアホだったわけじゃない。
当時の人間からしたら、その時のより良い方法を選んだだけ。
逆に、この当時の時点で、何年も先の将来の変化を見越して作るのはアホ。
「早すぎる一般化はだめ」
噛み合わなくなった時の対応には2つある「1. 部分的に改善する」「2. 全てを作り直す」
どっちが良い悪いではない。要はバランスであり、両極端になると問題になりがち。
ちょっとした変更でいい箇所を、「ええい、この際全部刷新だ!」とかいうのはアホ。
でも、全部刷新しないと完全に解決できないのに、部分的な対処療法で頑張るのも無理ある。
でも、じゃあ、どうやって判断するの?どっちのタイプの改修をするかって。
刷新したら「2倍以上良くなるか?」を定量的に測って決めろ
現行システムよりも2倍良くなる新システムになると確信できるなら、作り替えてしまおう。
ただし、確実に正確は無理でも、定量的に2倍良くなることの指標を割り出す必要がある。
曖昧な感じで、最新の流行に追うため!とかいう理由でやるな。(少なくとも枯れ始めるまで待てよ...)
2倍良くなると言えないなら、インクリメンタル的に部分的に修正してしまいなさい。
なお、刷新の際に小さな問題を一気に修正できるよう、日頃からissue化しておくと良いでしょう。
細かい問題が上がると思うが、すぐやるようなことではない。みたいなことよくある。(テストめんどいし)
でも、大規模改修になるなら、テストは結局めっちゃすることになる。
それなら、小さい問題も一気に修正してしまって、テストも一緒にしちゃう方が効率的だろう。
なので、日頃から小さな問題は記録して残しとけ。
13章:雪崩を起こした小石を探せ
バグ修正の流れの基本をまずは抑える
1. バグが発見される
2. バグの原因を探す
3. バグを修正する
4. バグ修正によって、新しく別のバグが入り込んでないかテストをし、コミットする
一番面倒なのは、「2」の部分。
バグの原因を中々探せないときつい。そして、そういうことはよくある
バグを再現できたら、原因も見つけやすい。しかし、再現するのが難しい状態では苦労する。
「バグをすぐに再現できるかどうか」が肝になってくる。
そういう意識を持っておき、この目標を素早く達成できるような設計を心がけると良い。
一例として、バグが起きた時に備えて、主要な箇所の引数をロギングしておくとか。
全てのAPIリクエストの引数とリクエストIDなんかを記録しといて、バグと繋ぎ合わせたりすると再現しやすいかも
しかし、、、問題は「状態」。状態が関わってくると、再現は途端に難しくなる。
「状態」を設計からできる限り排除しろ
バグが起きた際の「状態」の値の再現は、めっちゃ大変。
「状態」を利用/更新してる部品が多いほど、それが顕著になる。
結果が与えられる引数だけに依存してたら、誰が悪いかすぐわかる。
契約違反の引数を与えてるなら呼び出し元、契約通りの引数を与えてるなら呼び出し先。
対して「状態」に依存してると、その状態の数だけ、悪者の対象候補が増える。
それでも「状態」が必要な時はくる。そういう時は、デバッグが簡単にできるような工夫を施しておけ
「状態」の変更とかした際は、assertを使って内部テストをしておく。本番環境でも有効に。
「状態」が関わるところだけ、専用ツールとかでデバッグできるようにしておく。
などなど、そういうバグが起きた時、そのバグを再現しやすい工夫をしておけ。
todo.icon 自分のプロジェクトに、予期せぬバグを再現できる仕組みを考えて施す
14章:コードには4つの種類がある
4つの種類とは!?
1. 「やさしい」問題に対して単純な方法で解くコード
2. 「やさしい」問題に対して複雑な方法で解くコード
3. 「難しい」問題に対して単純な方法で解くコード
4. 「難しい」問題に対して複雑な方法で解くコード
「やさしい」問題を、単純な方法で解くのが、プログラマとして最低限の心得
やさしい問題を複雑な方法で解いてしまうようでは、まだまだ甘い。半人前にもなってない。
無駄に一般化して複雑に読みにくくするとか、よくある。そういうのは避けよう。
最低限の優秀さは保持するんだ。
「やさしい」〜「難しい」のレベルは連続的なものであり、優秀なプログラマほど、より難しい問題を単純に解ける
プログラマの優秀さは定量化でき、より難しい問題を単純に解ければ解けるほど優秀。
逆に、比較的やさしいレベルの問題も単純に解けないのは、平凡以下のプログラマということになる。
todo.icon より高みを目指すためには、やはり知識獲得と経験と思考を怠らないこと
アルゴリズム/データ構造の勉強は無駄とか言われてるが、全く無駄ではないonigiri.w2.icon
15章:雑草は抜け
雑草てのは、周りに影響なく抜けて、尚且つ綺麗にできる。コード管理もそれと一緒。
抜いても修正しても、全く周りに影響がない。けれど、それを修正したら、長期的に少し利益がある。
そういうのは、見つけたらすぐ抜いていけ。
放置していても短期的には影響ないし、ほとんどの場合問題ない。
しかし、その雑草はいつか牙を剥いてくる。たった1, 2分やろう。見つけたら抜け。
変数名とかおかしかったり、コメントと関数の内容が不一致だったり、そういう綺麗にしても問題ない箇所は、見つけたら抜け。
ただ...変数名の変更は意外と問題になったりすることもあるから気をつけて。
誰かが内部依存とかしてたら終わる。(でも、そもそもクソな行為を許すなよ)
16章:コードから先に進むのではなく、結果から後ろ向きにたどれ
つまり、技術ベースで手段を選ぶばかりではなく、問題にも立ち返って、より効率の良い方法を選べ
プログラマは、技術ベースで安易に解決策を選びがちである。
それによって、より良い方法を失ったりするので注意が必要。
常に、問題に対してより良い解決策はないかと自問せよ。
17章:大きい問題ほど解決しやすいこともある
ちょっと何言ってるかわかりませんでした...wonigiri.w2.icon
とりあえず、「基本的には特殊な方法を使わず、より単純な方法を」って話ではある。
しかし、それを差し置いてもなお、よりプリミティブで、ブリティッシュなやり方があるかもしれない。
その時は、「それ使ってみようぜ⭐」️みたいな話っぽい。
18章:コードに自ら物語を語らせろ
本を読むようにコードを読める状態にしよう。志せ。
何度も口酸っぱく言う。本を読むようにすらすら読めるようにしろ。認知負荷を下げろ。
プログラマの仕事は大概、コードを読むこと。
1年後の自分が読んでも、すらすら理解できる状態を保て!!!馬鹿たれ。
コメントの記入はリスクがある
「コメント」は、コードを本のようにスラスラ読めるようにするための、追加オプションである。
正しく使えれば、コードだけでもある程度簡単に読めてたものを、もっと読みやすくできる。
ただし、使い方を間違えれば、「読みにくくなる」、かつ、「負債になる」。
負債になる?
コメントのミスは、コンパイラがチェックできない。
つまり、コードの修正時などで、コメントの修正も一緒にすべきなのだが、それを忘れると放置される。
そうなると、何ヶ月か後に別の人が同じコードを読んだ際、コメントとコードの間に乖離が見られることになる。
これによって、間違った使われ方がされてしまい、バグに繋がることになる。
と言うことで、コメントを使う際は、こう言うリスクがあることを念頭に置いておけ。
正しい使い方は?
コードで表現できない、かつ、そのコメントを追加したら読みやすくなる場合、コメントを置いておけ。
基本的には、コードを段落に区切る感じで見出し的な使い方したり。
あとは、「なぜそのコードにしたか?」のような、目的や意図を書くのがセオリー。
ただし、これに関しても、ちゃんとメンテナンスしないと、人為ミスの原因になるから注意せよ。
20章:事前に定量的に計算をしろ!
プログラマは、目の前の効率化できそうで楽しそうなものがあると、すぐ飛びつきがち。やめろ。
その自動化は本当に意味あるんか?その最適化は本当に効果的なんか?
サボらずに、ちゃんと定量化しろ。手動化のコスト vs. 自動化コスト - 自動化によるリターン。
最適化もちゃんと計算してから意思決定しろ。サボるな。
21章:長期的に便益があるとわかってるなら、退屈なことでも頑張ってやれ
ついつい楽しいことに手を出しに行って、退屈なことから目を逸らしがちだな。
ワクワクすることを次から次に手を出したいのはわかる。
でも、「リターン」と言うものにもワクワクしろ。
長期的に見たらめっちゃリターンあるけど、すんごい繰り返し作業で退屈みたいなことあるよな?
ああ言うのは、誰かやらんとあかん。やっちまいな。それが後の3巡を買うことになる。
「長期的に考えろ」。OLTPとかで言われてたやろ。